#region Namespaces

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data;
using System.IO;
using System.Linq;
using System.Text;

using Vibstudio.X4NET.Parsing;

#endregion

namespace Vibstudio.X4NET.Csv
{
    /// <summary>
    /// Csv document.
    /// </summary>
    public class CsvDocument
    {
        #region Private members

        private static readonly CsvConfig defaultSettings = new CsvConfig();

        private LexList _parser;
        private CsvConfig _settings;

        #endregion

        #region Public Properties

        /// <summary>
        /// Column names.
        /// </summary>
        public ReadOnlyCollection<string> Columns { get; private set; }

        /// <summary>
        /// Records as a list of dictionaries.
        /// </summary>
        public List<CsvDictionary> Data { get; private set; }

        public string Header
        {
            get
            {
                if (!_settings.ContainsHeaders)
                {
                    return "";
                }

                StringBuilder builder = new StringBuilder();
                foreach (var key in Data[0].Keys)
                {
                    builder.AppendFormat("{0}{1}", key, _settings.Separator);
                }
                builder.Length--;
                return builder.ToString();
            }
        }

        public string FirstRow
        {
            get { return GetRow(1); }
        }

        public string EmptyRow
        {
            get
            {
                StringBuilder builder = new StringBuilder();
                for (int i = 0; i < Columns.Count; i++)
                {
                    builder.AppendFormat("{0}", _settings.Separator);
                }
                builder.Length--;
                return builder.ToString();
            }
        }

        /// <summary>
        /// Content of csv.
        /// </summary>
        public string Content { get; private set; }

        #endregion
        
        #region Column Iteration

        /// <summary>
        /// Iterate over each column value using the column name.
        /// </summary>
        /// <param name="columnName"></param>
        /// <param name="action"></param>
        public void ForEach(string columnName, Action<int, int, string> action)
        {
            ForEach<string>(columnName, 0, action);
        }
        
        /// <summary>
        /// Iterate over each column value.
        /// </summary>
        /// <param name="columnName"></param>
        /// <param name="startingRow"></param>
        /// <param name="action"></param>
        public void ForEach(string columnName, int startingRow, Action<int, int, string> action)
        {
            ForEach<string>(columnName, startingRow, action);
        }

        /// <summary>
        /// Iterate over each column value.
        /// </summary>
        /// <param name="column"></param>
        /// <param name="action"></param>
        public void ForEach(int column, Action<int, int, string> action)
        {
            ForEach<string>(column, 0, action);
        }
        
        /// <summary>
        /// Iterate over each column string values.
        /// </summary>
        /// <param name="column"></param>
        /// <param name="startingRow"></param>
        /// <param name="action"></param>
        public void ForEach(int column, int startingRow, Action<int, int, string> action)
        {
            ForEach<string>(column, startingRow, action);                
        }
        
        /// <summary>
        /// Iterate over each column values.
        /// </summary>
        /// <param name="columnName"></param>
        /// <param name="startingRow"></param>
        /// <param name="action"></param>
        public void ForEach<T>(string columnName, int startingRow, Action<int, int, T> action)
        {
            int column = Columns.IndexOf(columnName);
            if (column < 0) throw new ArgumentException("Unknown column name : " + columnName);
            ForEach(column, startingRow, Data.Count - 1, action);
        }
        
        /// <summary>
        /// Iterate over each column values.
        /// </summary>
        /// <param name="column"></param>
        /// <param name="startingRow"></param>
        /// <param name="action"></param>
        public void ForEach<T>(int column, int startingRow, Action<int, int, T> action)
        {
            ForEach(column, startingRow, Data.Count - 1, action);
        }

        /// <summary>
        /// Iterate over each column values.
        /// </summary>
        /// <param name="column"></param>
        /// <param name="startingRow"></param>
        /// <param name="endingRow"></param>
        /// <param name="action"></param>
        public void ForEach<T>(int column, int startingRow, int endingRow, Action<int, int, T> action)
        {
            if (column < 0 || column >= Columns.Count) throw new IndexOutOfRangeException("Column index out of range : " + column);
            if (startingRow < 0 || startingRow >= Data.Count) throw new IndexOutOfRangeException("StartRow index out of range : " + startingRow);
            if (endingRow < 0 || endingRow < startingRow || endingRow >= Data.Count) throw new IndexOutOfRangeException("EndRow index out of range : " + endingRow);

            for (int row = startingRow; row <= endingRow; row++)
            {
                T val = Get<T>(row, column);
                action(row, column, val);
            }
        }

        #endregion
        
        #region DataTable Conversion

        /// <summary>
        /// Convert the document to a datatable.
        /// </summary>
        /// <returns></returns>
        public DataTable ToDataTable()
        {
            return ToDataTable("csv.data");
        }
        
        /// <summary>
        /// Convert the document to a DataTable w/ the specified name.
        /// </summary>
        /// <param name="tableName">Name to apply to datatable.</param>
        /// <returns>System.DataTable</returns>
        public DataTable ToDataTable(string tableName)
        {
            return ToDataTable(tableName, 0, Data.Count - 1, 0, Columns.Count - 1);
        }

        /// <summary>
        /// Convert the document to a DataTable w/ the specified name.
        /// This starts at the specified row/column and includes all the rows/columns after it.
        /// </summary>
        /// <param name="tableName">Name to apply to table</param>
        /// <param name="startRow">Row to start at.</param>
        /// <param name="startCol">Column to start at.</param>
        /// <returns>System.DataTable</returns>
        public DataTable ToDataTable(string tableName, int startRow, int startCol)
        {
            return ToDataTable(tableName, startRow, Data.Count - 1, startCol, Columns.Count - 1);
        }
        
        /// <summary>
        /// Convert the document to a DataTable w/ the specified name.
        /// This starts at the specified row/column and includes all the rows/columns after it.
        /// </summary>
        /// <param name="tableName">Name to apply to table</param>
        /// <param name="startRow">Row to start at.</param>
        /// <param name="endRow">Row to end at.</param>
        /// <param name="startCol">Column to start at.</param>
        /// <param name="endCol">Column to end at.</param>
        /// <returns>System.DataTable</returns>
        public DataTable ToDataTable(string tableName, int startRow, int endRow, int startCol, int endCol)
        {
            var table = new DataTable(tableName);

            // Add columns.
            for (int ndxCol = startCol; ndxCol <= endCol; ndxCol++)
            {
                string columnName = Columns[ndxCol];
                table.Columns.Add(columnName);
            }

            // Now add data.
            for(int ndxRow = startRow; ndxRow <= endRow; ndxRow++)
            {
                CsvDictionary row = Data[ndxRow];
                DataRow newRow = table.NewRow();
                for (int ndxCol = startCol; ndxCol <= endCol; ndxCol++)
                {
                    string cellVal = row[ndxCol] as string;
                    newRow[ndxCol] = cellVal;
                }
                table.Rows.Add(newRow);
            }
            return table;
        }

        #endregion

        #region Public methods

        public void LoadCsv(string csv)
        {
            Init(csv, false);
        }

        public void LoadCsv(string csv, bool hasHeaders)
        {
            CsvConfig settings = new CsvConfig { ContainsHeaders = hasHeaders };
            Init(csv, false, settings, true);
        }

        public void LoadCsv(string csv, char delimiter)
        {
            CsvConfig settings = new CsvConfig { Separator = delimiter };
            Init(csv, false, settings, true);
        }

        public void LoadCsv(string csv, bool hasHeaders, char delimiter)
        {
            CsvConfig settings = new CsvConfig { ContainsHeaders = hasHeaders, Separator = delimiter };
            Init(csv, false, settings, true);
        }

        public void Load(string path)
        {
            Init(path, true);
        }

        public void Load(string path, bool hasHeaders)
        {
            CsvConfig settings = new CsvConfig { ContainsHeaders = hasHeaders };
            Init(path, true, settings, true);
        }

        public void Load(string path, char delimiter)
        {
            CsvConfig settings = new CsvConfig { Separator = delimiter };
            Init(path, true, settings, true);
        }

        public void Load(string path, bool hasHeaders, char delimiter)
        {
            CsvConfig settings = new CsvConfig { ContainsHeaders = hasHeaders, Separator = delimiter };
            Init(path, true, settings, true);
        }

        /// <summary>
        /// Returns all the records in the csv content.
        /// </summary>
        /// <returns></returns>
        public List<List<string>> Parse()
        {
            List<List<string>> data = _parser.ParseLines(Content);
            return data;
        }

        /// <summary>
        /// Write csv doc to the file in csv format.
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="delimeter"></param>
        public void Write(string fileName, string delimeter)
        {
            Write(fileName, delimeter, false, "\"", Environment.NewLine, false);
        }
        
        /// <summary>
        /// Write csv doc to the filename.
        /// </summary>
        /// <param name="fileName">FileName</param>
        /// <param name="delimeter">Delimeter to use</param>
        /// <param name="quoteAll">Quote all fields</param>
        /// <param name="quoteChar">Character to use for strings when quoting.</param>
        /// <param name="newLine">New Line character to use.</param>
        /// <param name="append"></param>
        public void Write(string fileName, string delimeter, bool quoteAll, string quoteChar, string newLine, bool append)
        {
            List<string> columns = new List<string>(Columns);
            List<List<object>> data = new List<List<object>>();
            foreach (CsvDictionary rec in Data)
            {
                var recToAdd = new List<object>();
                for (int ndx = 0; ndx < rec.Count; ndx++) recToAdd.Add(rec[ndx].ToString());
                data.Add(recToAdd);
            }
            //Csv.Write(fileName, data, delimeter, columns, false, quoteAll, quoteChar, Environment.NewLine, append);
            using (var writer = new CsvWriter(fileName, data, delimeter, columns, false, quoteAll, quoteChar, newLine, append))
            {
                writer.Write();
            }
        }

        /// <summary>
        /// Get the value at the row/column index.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="rowPos"></param>
        /// <param name="colPos"></param>
        /// <returns></returns>
        public T Get<T>(int rowPos, int colPos)
        {
            T emptyResult = (T)"".ToObject<T>();

            if (rowPos < 0) return emptyResult;
            if (rowPos > Data.Count) return emptyResult;

            if (colPos < 0) return emptyResult;

            if (_settings.ContainsHeaders && rowPos == 0)
            {
                ICollection keys = Data.First().Keys;
                int i = 1;
                foreach (var key in keys)
                {
                    if (i == colPos) return (T)key.ToObject<T>();
                    i++;
                }

                return emptyResult;
            }

            if (colPos > Data[rowPos - 1].Count) return emptyResult;

            string result = (string)Data[rowPos - 1][colPos - 1];
            T typedResult = (T)result.ToObject<T>();
            return typedResult;
        }

        ///// <summary>
        ///// Get the value at the row/column name.
        ///// </summary>
        ///// <typeparam name="T"></typeparam>
        ///// <param name="rowPos"></param>
        ///// <param name="colName"></param>
        ///// <returns></returns>
        //public T Get<T>(int rowPos, string colName)
        //{
        //    string result = (string)Data[rowPos][colName];
        //    T typedResult = (T)Converter.ConvertObj<T>(result);
        //    return typedResult;
        //}
        
        public string GetRow(int rowPos)
        {
            if (_settings.ContainsHeaders)
            {
                if (rowPos == 0) return Header;
                if (rowPos > Data.Count) return "";
            }
            else
            {
                if (rowPos == 0) return "";
                if (rowPos > Data.Count) return "";
            }

            StringBuilder rowBuilder = new StringBuilder();

            CsvDictionary dataRow = Data[rowPos - 1];
            foreach (var value in dataRow.Values)
            {
                rowBuilder.AppendFormat("{0}{1}", value, _settings.Separator);
            }
            rowBuilder.Length--;

            return rowBuilder.ToString();
        }

        #endregion

        #region Helpers

        private void Init (string contentOrFilePath, bool isFilePath)
        {
            Init(contentOrFilePath, isFilePath, defaultSettings, true);
        }

        private void Init(string contentOrFilePath, bool isFilePath, CsvConfig settings, bool autoLoad)
        {
            if (isFilePath && !File.Exists(contentOrFilePath))
                throw new IOException(string.Format("Csv file : {0} does not exist.", contentOrFilePath));

            Content = isFilePath ? File.ReadAllText(contentOrFilePath) : contentOrFilePath;
            _settings = settings;

            var lexListSettings = new LexListSettings { MultipleRecordsUsingNewLine = true, Delimeter = settings.Separator };

            // If the separator is the tab character, do not consider the tab as a whitespace character.
            if (settings.Separator == '\t')
                lexListSettings = new LexListSettings
                {
                    MultipleRecordsUsingNewLine = true,
                    Delimeter = settings.Separator,
                    WhiteSpaceChars = new[] { ' ' }
                };

            _parser = new LexList(lexListSettings);
            if (autoLoad)
            {
                ParseDict();
            }
        }

        /// <summary>
        /// Parse as list of records where each record is a dictionary.
        /// This should be used if the first column has headers.
        /// </summary>
        /// <returns></returns>
        private void ParseDict()
        {
            List<List<string>> records = Parse();
            // Columns
            List<string> columnNames;
            if (_settings.ContainsHeaders)
            {
                columnNames = records[0];
            }
            else
            {
                columnNames = new List<string>();
                for (int col = 0; col < records[0].Count; col++) columnNames.Add(col.ToString());
            }

            List<CsvDictionary> tableData = new List<CsvDictionary>();
            int startingDataRecord = _settings.ContainsHeaders ? 1 : 0;
            for (int ndx = startingDataRecord; ndx < records.Count; ndx++)
            {
                List<string> record = records[ndx];
                CsvDictionary recordMap = new CsvDictionary();

                // Assert that each record has same number of columns
                if (record.Count != columnNames.Count) throw new ArgumentException(string.Format("Record at line {0} does not have same number of columns as in header.", (ndx + 1)));

                // Map the column names to record values.
                for (int colIndex = 0; colIndex < columnNames.Count; colIndex++)
                {
                    string colName = columnNames[colIndex];
                    //recordMap[colName] = record[colIndex];

                    recordMap.Add(colName, record[colIndex]);
                }

                // Now store record.
                tableData.Add(recordMap);
            }
            Data = tableData;
            Columns = new ReadOnlyCollection<string>(columnNames);
        }

        #endregion
    }
}
